

const { floor, random } = Math,
  // 最小值
  min = (...args) => args.reduce((p, c) => (p < c ? p : c), args[0]),
  // 最大值
  max = (...args) => args.reduce((p, c) => (p < c ? c : p), args[0]),
  // 范围在 min 到 max 随机整数
  randInt = (max, min = 0) => floor(random() * (max - min)) + floor(min),
  // 随机获取数组种的元素
  randItem = (...args) => args[randInt(args.length)];

/**
 * createEmitter
 */
function createEmitter() {
  const listeners = {};
  return {
    on(key, fn, scope) {
      listeners[key] = (listeners[key] || []).concat({ fn, scope });
    },
    off(key, fn) {
      if (key == "*") listeners[key] = [];
      else listeners[key] = (listeners[key] || []).filter((l) => l.fn !== fn);
    },
    emit(key, data) {
      (listeners[key] || []).forEach(function ({ fn, scope }) {
        fn.call(scope, data);
      });
    },
  };
}

/**
 * tetris 砖块
 *
 * 俄罗斯方块中，组成各个砖块的最小正方形基本单元称为方块（Tile）；
 * 由若干个方块（Tile）组成的形状，称为砖块（Tetris）。
 * 被玩家或AI操控的砖块（Tetris），称为活动砖块（Active Tetris）。
 *
 * 移动：move
 * 旋转：rotate，
 *    旋转参考 SRS 规则，则出现方块数组中坐标出现负值情况。
 *    为实现方便，算法将不对负值情况进行修正处理。
 *    数组中最小值不再为零可以出现负值情况，需要对算法调整。
 *    且绘制背景时，也要能兼容负值情况。
 *
 * 基本的子类型： S，T，J，L，O，Z，I
 */
class Tetris {
  tetris;
  color;

  constructor(top, left, rows, columns, background) {
    // 对象属性赋值
    this.rows = rows;
    this.columns = columns;
    this.background = background;
    // position: 旋转中心点所在 background 的位置
    this.position = { top, left };
    // 模拟事件处理函数通知 Game 类型实例处理方块的销毁事件
    const { on, off, emit } = createEmitter();
    this.on = on;
    this.off = off;
    this.emit = emit;
  }
  onStop(fn, scope) {
    this.on("onStop", fn, scope);
  }
  offStop(fn) {
    this.off("onStop", fn);
  }
  /**
   * collisionDetection
   *
   * 碰撞检测函数，返回布尔类型。
   *
   * @param {array} tetris 砖块
   * @param {number} top
   * @param {number} left
   *
   * @returns boolean 真值表示通过检测
   */
  collisionDetection(tetris, top, left) {
    const { rows, columns, background, minColumn, maxColumn, maxRow } = this;
    return !(
      left + maxColumn > columns ||
      left + minColumn < 0 ||
      top + maxRow >= rows - 1 ||
      tetris.some(
        ([row, column]) =>
          top + row >= 0 && background[top + row][left + column] != 0
      )
    );
  }
  /**
   * move
   * 改变方块的位置，并且进行碰撞检测。
   *
   * @param {string} direction left | right | down | direct
   */
  move(direction) {
    const {
      position: { left, top },
      tetris,
    } = this;
    if (direction == "left") {
      if ( this.collisionDetection(tetris, top, left - 1)) {
        this.position.left -= 1;
      }
    } else if (direction == "right") {
      if ( this.collisionDetection( tetris, top, left + 1)) {
        this.position.left += 1;
      }
    } else if (direction == "down" || direction == "direct") {
      do {
        if (
          this.collisionDetection(
            tetris.map(([row, column]) => [row + 1, column]),
            this.position.top,
            left
          )
        ) {
          this.position.top += 1;
        } else {
          // 无法下坠，消息通知方块实例已经不能移动
          this.emit("onStop");
          if (direction == "direct") break;
        }
      } while (direction == "direct");
    }
  }
  /**
   * rotate 旋转方块
   * 根据 SRS 的旋转规则，可以实现 Z S J L T 的旋转计算。
   *
   * @param {string} direction left | right
   */
  rotate(direction = "left") {
    // 根据公式进行映射：x1 = cos(a) * x - sin(a) * y, y1 = cos(a) * y + sin(a) * x
    // 则左旋 a = 90, 所以：x1 = y, y = -x
    let tetris =
      direction == "left"
        ? this.tetris.map(([row, column]) => [-column, row])
        : this.tetris.map(([row, column]) => [column, -row]);
    // 进行碰撞检测：不能超出左右边界且在背景中没有被阻挡。
    const {
      position: { left, top },
    } = this;
    if (this.collisionDetection(tetris, top, left)) {
      this.tetris = tetris;
    }
  }
  get minColumn() {
    return min(...this.tetris.map(([, column]) => column));
  }
  get maxColumn() {
    return max(...this.tetris.map(([, column]) => column));
  }
  get minRow() {
    return min(...this.tetris.map(([row]) => row));
  }
  get maxRow() {
    return max(...this.tetris.map(([row]) => row));
  }
  /**
   * width 当前方块的宽度  * 使用 get 属性写法实现
   */
  get width() {
    return this.maxColumn - this.minColumn + 1;
  }
  /**
   * height 当前方块的高度
   */
  get height() {
    return this.maxRow - this.minRow + 1;
  }
  /**
   * 使用生成器函数实现迭代器，返回在容器背景的真实位置！
   */
  *[Symbol.iterator]() {
    for (let [row, column] of this.tetris) {
      yield [this.position.top + row, this.position.left + column];
    }
  }
}

/**
 * 基本砖块：
 * T, J, L, Z, S, O, I,
 */
class T extends Tetris {
  color = "purple";
  tetris = [
    [0, 0],
    [0, 1],
    [0, -1],
    [-1, 0],
  ];
}

class J extends Tetris {
  color = "blue";
  tetris = [
    [0, -1],
    [0, 0],
    [0, 1],
    [-1, -1],
  ];
}

class L extends Tetris {
  color = "orange"
  tetris = [
    [0, -1],
    [0, 0],
    [0, 1],
    [-1, 1],
  ]
}

class S extends Tetris {
  color = "green";
  tetris = [
    [0, 0],
    [0, -1],
    [-1, 0],
    [-1, 1],
  ];
}

class Z extends Tetris {
  color = "red";
  tetris = [
    [-1, -1],
    [-1, 0],
    [0, 0],
    [0, 1],
  ];
}

/**
 * O型方块
 * 使用公式旋转，不论如何都存在修正的。
 * 但其实 O 型方块不需要旋转，因此直接覆盖父类旋转方法。
 */
class O extends Tetris {
  color = "yellow";
  tetris = [
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1],
  ];
  // o 不需要进行旋转
  rotate() {}
}

/**
 * I 按照SRS规则，也有四种位置。
 * 但I和O一样无法找到中心点，此处以第二个方块为中心点。
 * 在旋转完成后，对位置进行修正。
 */
class I extends Tetris {
  color = "cyan";
  tetris = [
    [0, -1],
    [0, 0],
    [0, 1],
    [0, 2],
  ];
  rotate(direction = "left") {
    const tetris =
      direction == "left"
        ? this.tetris.map(([row, column]) => [-column, row])
        : this.tetris.map(([row, column]) => [column, -row]);
    // 修正坐标
    // 进行碰撞检测
    const {
      position: { left, top },
    } = this;
    if (this.collisionDetection(tetris, top, left)) {
      this.tetris = tetris;
    }
  }
}

/**
 * Timer
 *
 * 计时器类，记录毫秒数。精度：100毫秒
 *
 * 开始：start
 * 暂停: stop
 * 重置: reset
 */
class Timer {
  constructor(delay = 100) {
    this.delay = delay;
    // 累计毫秒数
    this._count = 0;
    this.timerId = undefined;
    // 计时更新事件
    const { on, off, emit } = createEmitter();
    this.on = on;
    this.off = off;
    this.emit = emit;
  }
  start() {
    if (!this.timerId) {
      this.timerId = setInterval(() => {
        this._count += this.delay;
        this.emit("onUpdate", this._count);
      }, this.delay);
    }
  }
  stop() {
    if (this.timerId) {
      clearInterval(this.timerId);
      this.timerId = undefined;
    }
  }
  reset() {
    this.stop();
    this._count = 0;
  }
  get count() {
    return this._count;
  }
  onUpdate(fn, scope) {
    this.on("onUpdate", fn, scope);
  }
  offUpdate(fn) {
    this.off("onUpdate", fn);
  }
}

/**
 * randTetris
 * 产生随机砖块
 *
 * @param {number} rows 总行数
 * @param {number} columns 总列数
 * @param {array} background 背景数组
 * @returns Tetris
 */
const randTetris = (rows, columns, background) => {
  const tetris = new (randItem(T, J, O, S, L, I, Z))(
    0,
    floor(columns / 2),
    rows,
    columns,
    background
  );
  tetris.position.left = floor(columns / 2) - floor(tetris.width / 2);
  return tetris;
};

const Status = {
    init: Symbol("init"),
    running: Symbol("running"),
    pause: Symbol("pause"),
    end: Symbol("end"),
  },
  DefaultKeymaps = {
    moveLeft: "ArrowLeft",
    moveRight: "ArrowRight",
    moveDown: "ArrowDown",
    rotateLeft: "ArrowUp",
    rotateRight: "KeyB",
    direct: "Space",
    pause: "KeyP",
  };
/**
 * Game 控制游戏、绘制画面
 *
 * 游戏背景: background = [rows, columns]
 *
 * 开始游戏：start
 * 暂停游戏：pause
 * 终止游戏：end
 *
 * 算法：游戏结束检测、记录得分
 */
class Game {
  /**
   * 构造函数
   *
   * @param {HTMLElement} container
   * @param {number} width 300
   * @param {number} height 600
   * @param {number} rows 20
   * @param {number} columns 10
   */
  constructor(
    container,
    {
      width = 300,
      height = 600,
      rows = 20,
      columns = 10,
      keymaps = DefaultKeymaps,
    } = {}
  ) {
    // 保存实例属性
    this.container = container;
    this.width = width;
    this.height = height;
    this.rows = rows;
    this.columns = columns;
    this.keymaps = keymaps;
    // 初始化 background 数组 [rows, columns]
    this.background = [];
    // 生成 canvas 的 DOM 对象
    const el = document.createElement("canvas");
    el.width = width;
    el.height = height;
    container.appendChild(el);
    // 获取 context 对象
    this.ctx = el.getContext("2d");
    // 获取合理的方块长度
    this.size = min(width / columns, height / rows);
    // 当前状态：未开始(init)、游戏中(running)、暂停(pause)、结束(end)
    this.status = Status.init;
    // 游戏计分
    this.score = 0;
    // 时间累计
    this.timer = new Timer();
    // active 当前砖块，next 下一个随机砖块
    this.active = undefined;
    this.next = undefined;
    // 下坠间隔时间
    this.speed = 1000;
    // 状态更新事件: update
    const { on, off, emit } = createEmitter();
    this.on = on;
    this.off = off;
    this.emit = emit;
    // 注册键盘监听事件
    const actions = {
      moveLeft: () => this.active.move("left"),
      moveRight: () => this.active.move("right"),
      moveDown: () => this.active.move("down"),
      rotateLeft: () => this.active.rotate("left"),
      rotateRight: () => this.active.rotate("right"),
      direct: () => this.active.move("direct"),
      pause: () => this.pauseOrResume(),
    };
    const onKeyDown = (event) => {
      Object.entries(this.keymaps).forEach(([key, value]) => {
        if (value == event.code) {
          if (
            (this.status == Status.pause && key == "pause") ||
            this.status == Status.running
          ) {
            actions[key]?.();
          }
        }
      });
      // console.log(event.code);
    };
    document.addEventListener("keydown", onKeyDown);
    //
    this.timer.onUpdate(this.autoDown, this);
    // destroy 销毁游戏
    this.destroy = () => {
      // 移除键盘监听事件函数
      document.removeEventListener("keydown", onKeyDown);
      //
      this.timer.offUpdate("*");
    };
  }
  onUpdate(fn, scope) {
    this.on("onUpdate", fn, scope);
  }
  offUpdate(fn) {
    this.off("onUpdate", fn);
  }
  autoDown() {
    if (this.status != Status.running) return;
    // 改变下坠速度
    if (
      this.speed > 200 &&
      this.timer.count > 0 &&
      this.timer.count % (30 * 1000) == 0
    ) {
      this.speed -= 100;
    }
    // 累计时间
    this.cumulativeTime += 100;
    if (this.cumulativeTime >= this.speed) {
      this.cumulativeTime = 0;
      this.active.move("down");
    }
  }
  // 初始化游戏状态
  init() {
    const { rows, columns, background } = this;
    this.status = Status.init;
    this.score = 0;
    // 清除所有数组元素
    this.background.length = 0;
    // 初始化背景
    this.background.unshift(
      ...new Array(rows).fill(0).map(() => new Array(columns).fill(0))
    );
    // 重置计时器
    this.timer.reset();
    this.speed = 1000;
    // 随机当前活动砖块和下一个砖块
    this.active = randTetris(rows, columns, background);
    this.active.onStop(this.doTetrisStop, this);
    this.next = randTetris(rows, columns, background);
    // 累计秒数
    this.cumulativeTime = 0;
  }
  doTetrisStop() {
    const { rows, columns, background } = this;
    // 记录当前方块销毁的位置
    if (this.active) {
      for (let [row, column] of this.active) {
        if (row >= 0) background[row][column] = this.active.color;
      }
    }
    // 移除行
    for (let row = rows - 1; row >= 0; row--) {
      if (background[row].every((i) => i != 0)) background.splice(row, 1);
    }
    const lines = rows - background.length;
    if (lines) {
      this.score += lines * lines;
      background.unshift(
        ...new Array(lines).fill(0).map(() => new Array(columns).fill(0))
      );
    }
    // 将下一个砖块转为当前砖块
    this.active = this.next;
    this.active.onStop(this.doTetrisStop, this);
    // 随机新方块
    this.next = randTetris(rows, columns, background);
    // 游戏结束条件：新砖块的初始状态在背景中有颜色
    if (
      Array.from(this.active).some(
        ([row, column]) => row >= 0 && background[row][column] != 0
      )
    ) {
      this.end();
    }
    // 通知状态变化
    this.emit("onUpdate", this);
  }
  end() {
    // 设置游戏状态
    this.status = Status.end;
    // 停止计时
    this.timer.stop();
    // 取消自动下坠
    // this.offUpdate(this.autoDown);
  }
  /**
   * start 控制游戏开始
   */
  start() {
    if (this.status == Status.running || this.status == this.pause) {
      return alert("上局游戏未结束，无法开始新游戏！");
    }
    const { ctx, width, height, size, rows, columns, background } = this;
    // 初始化状态
    this.init();
    this.status = Status.running;
    // 自动下坠
    // draw 绘制游戏内容
    const draw = () => {
      if (this.status == Status.end) return;
      if (this.status == Status.pause) return requestAnimationFrame(draw);
      // 清除上一帧
      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, width, height);
      // 绘制背景
      for (let row = 0; row < rows; row++) {
        for (let column = 0; column < columns; column++) {
          if (row >= 0 && background[row][column] != 0) {
            ctx.fillStyle = background[row][column];
            ctx.fillRect(column * size, row * size, size, size);
          }
        }
      }
      // 绘制当前活动的方块
      ctx.fillStyle = this.active.color;
      for (let [row, column] of this.active) {
        ctx.fillRect(column * size, row * size, size, size);
      }
      // 绘制参考线
      // ctx.strokeStyle = "rgba(255, 255, 255, 1)";
      ctx.strokeStyle = "black";
      ctx.beginPath();
      for (let i = 0; i < rows; i++) {
        ctx.moveTo(0, i * size);
        ctx.lineTo(width, i * size);
      }
      for (let i = 0; i < columns; i++) {
        ctx.moveTo(i * size, 0);
        ctx.lineTo(i * size, height);
      }
      ctx.stroke();
      // 请求绘制下一帧游戏内容 (递归)
      requestAnimationFrame(draw);
    };
    // 绘制游戏第一帧
    requestAnimationFrame(draw);
    // 开始计时
    this.timer.start();
    // 更新状态
    this.emit("onUpdate", this);
  }
  /**
   * pauseOrResume 暂停或者恢复游戏
   */
  pauseOrResume() {
    if (![Status.running, Status.pause].includes(this.status)) return;
    // 改变状态，控制计时器
    this.status =
      this.status == Status.running
        ? (this.timer.stop(), Status.pause)
        : (this.timer.start(), Status.running);
    this.emit("onUpdate", this);
  }
}
